--[[----------------------------------------------------------------------------
Copyright (C) 2006  Matt Richard

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
-------------------------------------------------------------------------------]]

-- System to defer function calls until outside the combat lockdown
--
--      Usage:
--          --Obtain an instance
--          local LockdownLine = DongleStub("LockdownLine")
--          LockdownLine:Do(func,...)
--
--      LockdownLine offers 2 ways to call functions that are protected while in
--      combat.  In both methods, it returns true when the function was run immediately
--      and false when the function is queued until the PLAYER_REGEN_ENABLED event.
--
--      LockdownLine:Do(func,...)
--          If not in the combat lockdown, the function is run immediately.
--          Otherwise, it is queued and run when the PLAYER_REGEN_ENABLED event
--          fires.
--
--          Example:
--              LockdownLine:Do(RegisterUnitWatch,frame)
--
--      Shortcuts for certain frame methods exist that allow you to call 
--      Lockdownline[methodName](frame,...) instead of using Do.
--          
--          Example:
--              LockdownLine:SetAttribute(frame, "type", "action")
--              LockdownLine:SetPoint(frame, "CENTER", 0, 0)
--
--          Supported Methods:
--              SetAttibute, SetPoint, SetParent, SetWidth, SetHeight,
--              SetAllPoints, SetFrameLevel, SetFrameStrata, SetID,
--              SetScale, SetToplevel, Show, Hide, Raise, Lower, 
--              SetHitRectInsets, EnableMouse, EnableMousewheel,
--              EnableKeyboard
--
--          Note that with SetAttribute, it will check frame:CanChangeProtectedState()
--          so that it will not queue attribute changes when they have been unlocked 
--          by secure code.
--

--[[---------------------------------------------------------------------------
    DongleStub
-----------------------------------------------------------------------------]]
do
    local major = "DongleStub"
    local minor = tonumber(string.match("$Revision: 36 $", "(%d+)") or 1)

    local g = getfenv(0)

    if not g.DongleStub or g.DongleStub:IsNewerVersion(major, minor) then
        local lib = setmetatable({}, {
            __call = function(t,k) 
                if type(t.versions) == "table" and t.versions[k] then 
                    return t.versions[k] 
                else
                    error("Cannot find a library with name '"..tostring(k).."'", 2)
                end
            end
        })

        function lib:IsNewerVersion(major, minor)
            local entry = self.versions and self.versions[major]
            
            if not entry then return true end
            local oldmajor,oldminor = entry:GetVersion()
            
            return minor > oldminor
        end
        
        function lib:Register(new)
            local major,minor = new:GetVersion()
            if not self:IsNewerVersion(major, minor) then return false end
            local old = self.versions and self.versions[major]
            -- Run the new libraries activation
            if type(new.Activate) == "function" then
                new:Activate(old)
            end
            
            -- Deactivate the old libary if necessary
            if old and type(old.Deactivate) == "function" then
                old:Deactivate(new) 
            end
            
            self.versions[major] = new
        end

        function lib:GetVersion() return major,minor end

        function lib:Activate(old)
            if old then 
                self.versions = old.versions
            else
                self.versions = {}
            end
            g.DongleStub = self
        end
        
        -- Actually trigger libary activation here
        local stub = g.DongleStub or lib
        stub:Register(lib)
    end

end

--[[---------------------------------------------------------------------------
    LockdownLine
-----------------------------------------------------------------------------]]

local MAJOR_VERSION = "LockdownLine"
local MINOR_VERSION = tonumber(("$Rev: 36 $"):match("(%d)") or 1)

if not DongleStub:IsNewerVersion(MAJOR_VERSION,MINOR_VERSION) then return end

local LockdownLine = {}

function LockdownLine:GetClosure()
    local closure = table.remove(self.pool)
    if closure then 
        return closure 
    else
        local func,a1,a2,a3,a4,a5,a6,n,args
        return function(f,...)
            if not f and type(func) == "function" then
                if n == 0 then
                    return func()
                elseif n == 1 then
                    return func(a1)
                elseif n == 2 then
                    return func(a1,a2)
                elseif n == 3 then
                    return func(a1,a2,a3)
                elseif n == 4 then
                    return func(a1,a2,a3,a4)
                elseif n == 5 then
                    return func(a1,a2,a3,a4,a5)
                elseif n == 6 then
                    return func(a1,a2,a3,a4,a5,a6)
                elseif n > 6 then
                    return func(unpack(args))
                end
            elseif type(f) == "function" then
                func,n = f,select("#", ...)
                
                if n > 6 then
                    args = args or {}
                    for k in pairs(args) do
                        args[k] = nil
                    end
                    for i=1, n do
                        args[i] = select(i, ...) 
                    end
                else
                    a1,a2,a3,a4,a5,a6 = ...
                end
            end
        end
    end
end

function LockdownLine:Do(func, ...)
    if InCombatLockdown() then
        local f = self:GetClosure()
        f(func, ...)
        self:Enqueue(f)
        return false
    else
        func(...)
        return true
    end
end

function LockdownLine:Enqueue(f)
    table.insert(self.queue,f)
    self.frame:RegisterEvent("PLAYER_REGEN_ENABLED")
end

function LockdownLine:ProcessQueue()
    if not InCombatLockdown() then
        for i,f in ipairs(self.queue) do
            f()
            self.queue[i] = nil
            table.insert(self.pool,f)
        end
        self.frame:UnregisterEvent("PLAYER_REGEN_ENABLED")
    end
end

    
function LockdownLine:SetAttribute(frame, ...)
    if type(frame) == "table" and type(frame.SetAttribute) == "function" then
        if frame:CanChangeProtectedState() then
            frame:SetAttribute(...)
            return true
        else
            local f = self:GetClosure()
            f(frame.SetAttribute, frame, ...)
            self:Enqueue(f)
            return false
        end
    end
end

-- DongleStub required methods and registration
function LockdownLine:GetVersion()
    return MAJOR_VERSION, MINOR_VERSION
end

function LockdownLine:Activate(old)
    
    local frameMethods = {"SetPoint","SetParent","SetWidth",
        "SetHeight","SetAllPoints","SetFrameLevel","SetFrameStrata","SetID",
        "SetScale","SetToplevel","Show","Hide","Raise","Lower",
        "SetHitRectInsets","EnableMouse","EnableMousewheel","EnableKeyboard"}
    
    for i,method in pairs(frameMethods) do
        self[method] = function(self, frame, ...)
            if type(frame) == "table" and type(frame[method]) == "function" then
                return self:Do(frame[method], frame, ...)
            end
        end
    end

    if old then
        self.frame = old.frame
        self.queue = old.queue
        self.pool = old.pool

        --Upgrade the old lib
        for k in pairs(old) do
            old[k] = nil
        end
        
        for k,v in pairs(self) do
            old[k] = v
        end
    else
        self.frame = CreateFrame("Frame")
        self.queue = {}
        self.pool = {}
    end
    
    self.frame:SetScript("OnEvent",function()self:ProcessQueue()end)
end

DongleStub:Register(LockdownLine)